package com.xxjh.brs.sftp.service;

import cn.hutool.extra.ftp.FtpException;
import cn.hutool.extra.ssh.Sftp;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.SftpATTRS;
import com.xxjh.brs.ftp.common.api.enums.ResponseStatus;
import com.xxjh.brs.ftp.common.exceptions.ConditionException;
import com.xxjh.brs.ftp.common.utils.FileUtil;
import com.xxjh.brs.sftp.vo.FileVo;
import com.xxjh.brs.sftp.vo.FloderVo;
import com.xxjh.brs.sftp.common.predicate.SftpPredicate;
import com.xxjh.brs.sftp.common.utils.SftpConstantsUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.ObjectPool;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * @author CHENCHEN
 * @title a
 * @company 新兴际华科技（天津）有限公司
 * @create 2024-07-04 15:27
 */
@Slf4j
@Component
public abstract class AbstractSftpService {

    @Value("${sftp.localdownloaddir}")
    protected String localDownloadDir;

    protected Sftp sftpClient;

    protected String sftpParentPath;

    protected abstract ObjectPool<Sftp> getSftpClientPool();

    // 获取sftpClient客户端，现在是抽象方法的调用
    protected Sftp getSFTPClient(String rootPath){
        try {
            sftpClient = getSftpClientPool().borrowObject();
            sftpClient.cd(rootPath);
            sftpParentPath = rootPath;
            return sftpClient;
        } catch (Exception e) {
            e.printStackTrace();
            throw new ConditionException(ResponseStatus.FTP_GET_CLIENT_EXCEPTION);
        }
    }

    protected void setSeparateClient(Sftp separateClient, String rootPath){
        sftpClient = separateClient;
        sftpParentPath = rootPath;
    }

    /**
     * 通过FTP下载文件到本地。
     *
     * @param specifiedLayer 有值代表指定目录，无值检索全部。
     * @param existKey 跳过指定目录。
     *
     */
    protected void downloadSftp(String drictoryPath, String specifiedLayer, List<String> existKey) throws IOException {
        downloadDirectorySftp(sftpParentPath, localDownloadDir + drictoryPath + specifiedLayer, existKey);
    }

    /**
     * 通过FTP下载文件到本地目录。
     *
     * @param remoteDirPath FTP需要查找到目录，首次调用传入根路径
     * @param localDirPath 存放本地目录
     * @param existDirectory 已同步过地图目录
     * @return
     */
    private void downloadDirectorySftp(String remoteDirPath, String localDirPath, List<String> existDirectory) throws IOException {
        if(sftpClient != null){
            Files.createDirectories(Paths.get(localDirPath));
            log.info("文件路径: {}", remoteDirPath);
            // 获取远程目录下的所有条目
            List<ChannelSftp.LsEntry> entries = sftpClient.lsEntries(remoteDirPath,
                    entry -> !entry.getFilename().startsWith("."));
            for (ChannelSftp.LsEntry entry : entries) {
                if (Thread.currentThread().isInterrupted()) {
                    log.warn("定时任务被提前结束, 中断SFTP下载3D瓦片到本地目录");
                    return;
                }
                if(existDirectory != null && existDirectory.contains(entry.getFilename())) {
                    continue;
                }
                SftpATTRS attrs = entry.getAttrs();
                // 拼接文件路径
                String remoteFilePath = FileUtil.getRemoteFilePath(remoteDirPath, entry.getFilename());
                log.info("文件路径: {}", remoteFilePath);
                String localFilePath = Paths.get(localDirPath, entry.getFilename()).toString();
                if (attrs.isDir()) {
                    Files.createDirectories(Paths.get(localFilePath));
                    downloadDirectorySftp(remoteFilePath, localFilePath, existDirectory);
                } else {
                    File localFile = new File(localFilePath);
                    // 检查文件是否已存在（在下载操作之前）
                    if (localFile.exists()) {
                        log.info("本地文件已存在，跳过下载: {}", localFilePath);
                        continue;
                    }
                    try (BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(localFile))) {
                        log.info("Downloading file: {} to {}", remoteFilePath, localFilePath);
                        sftpClient.get(remoteFilePath, outputStream);
                        log.info("Downloaded: {} to {}", remoteFilePath, localFilePath);
                    }
                }
            }
        }
    }

    /**
     * FTP获取资源服务器指定文件夹最后更新时间
     *
     * @param drictoryPath 指定路径。
     * @param specifiedLayer 文件夹名称。
     * @return 当前文件夹属性类。
     */
    protected FloderVo lastUpdateFloderProperties(String drictoryPath, String specifiedLayer){
        return searchLastUpdateDirectorySftp(drictoryPath + specifiedLayer);
    }

    /**
     * 查找指定目录下最后一个修改的文件目录。
     *
     * @param remoteDirectory  指定FTP路径下文件目录。
     * @return 最后一个修改的文件目录名称和修改时间。
     * @throws IOException 在文件操作中可能发生的I/O异常。
     */
    private FloderVo searchLastUpdateDirectorySftp(String remoteDirectory){
        List<ChannelSftp.LsEntry> directories = sftpClient.lsEntries(remoteDirectory,
                entry -> !entry.getFilename().startsWith("."));
        ChannelSftp.LsEntry newestDirectory = null;
        long latestModificationTime = -1;
        for (ChannelSftp.LsEntry entry : directories) {
            SftpATTRS attrs = entry.getAttrs();
            if (attrs.isDir()) {
                long modificationTime = attrs.getMTime();
                if (modificationTime > latestModificationTime) {
                    latestModificationTime = modificationTime;
                    newestDirectory = entry;
                }
            }
        }
        if (newestDirectory != null) {
            SftpATTRS attrs = newestDirectory.getAttrs();
            log.info("找到最后一个修改文件 [{}], 上次修改时间是 [{}]", newestDirectory.getFilename(), attrs.getMTime());
            return new FloderVo(newestDirectory.getFilename(), attrs.getMTime());
        } else {
            log.info("没有找到目录。");
        }
        return null;
    }

    /**
     * 删除SFTP服务器指定目录及其所有内容（包括子目录和文件）。
     *
     * @param dirPath 要删除的目录的完整路径。
     * @return 如果目录及其内容成功删除则返回true，否则返回false。
     */
    protected boolean delDirAndFile(String dirPath){
        try {
            sftpClient.delDir(dirPath);
            return true;
        }catch (Exception e){
            return true;
        }
    }

    /**
     * 删除FTP指定文件目录及下面文件。
     *
     * @param rootPath 指定路径。
     * @param directoryPath 有值代表指定目录，无值检索全部。
     * @return 删除成功或失败。
     */
    public boolean deleteLocalDirectory(String rootPath, String directoryPath){
        try {
            //删除本地目录
            return FileUtil.deleteFolderContents(new File(localDownloadDir + rootPath + directoryPath));
        } catch (Exception e) {
            throw new ConditionException(ResponseStatus.FILE_LOCAL_DEL_EXCEPTION);
        }
    }

    /**
     * 本地文件上传到FTP服务器。
     * 支持多级目录文件
     *
     * @param drictoryPath FTP目标服务器根路径下具体某文件夹路径。
     * @param specifiedLayer 代表指定目录。
     *
     */
    protected void uploadLocalFtp(String drictoryPath, String specifiedLayer){
        try {
            uploadDirectory(drictoryPath + specifiedLayer, localDownloadDir + specifiedLayer);
        } catch (IOException e) {
            throw new ConditionException(ResponseStatus.FTP_UPLOADING_EXCEPTION);
        }

    }

    /**
     * 指定本地具体目录下文件上传到FTP服务器。
     * 支持多级目录文件
     *
     * @param drictoryPath FTP目标服务器根路径下具体某文件夹路径。
     * @param specifiedLayer 代表指定目录。
     *
     */
    protected void uploadLocalDirectoryFileFtp(String drictoryPath, String specifiedLayer){
        String remoteDirPath = drictoryPath + specifiedLayer; //远程路径
        mkdirsSftp(remoteDirPath);
        String localParentDir = localDownloadDir + specifiedLayer; //本地路径
        localParentDir = Paths.get(localParentDir).toString();
        File localDir = new File(localParentDir);
        String[] subFiles = localDir.list();
        if (subFiles != null && subFiles.length > 0) {
            for (String item : subFiles) {
                if (Thread.currentThread().isInterrupted()) {
                    log.warn("定时任务被提前结束, 中断递归上传指定本地目录到FTP资源服务器");
                    return; // 中断标记已设，提前结束循环
                }
                File localFile = new File(localParentDir, item);
                uploadFileToSftp(localFile, sftpParentPath + remoteDirPath, item);
            }
        }

    }

    /**
     * 递归上传指定本地目录到远程服务器目录。
     *
     * @param remoteDirPath 远程目录路径(所有文件上传的统一前缀)
     * @param localParentDir 本地文件路径(上传的本地目录)
     * @throws IOException 如果文件操作失败则抛出异常
     */
    protected void uploadDirectory(String remoteDirPath, String localParentDir) throws IOException {
        localParentDir = Paths.get(localParentDir).toString();
        File localDir = new File(localParentDir);
        String[] subFiles = localDir.list();
        if (subFiles != null && subFiles.length > 0) {
            for (String item : subFiles) {
                if (Thread.currentThread().isInterrupted()) {
                    log.warn("定时任务被提前结束, 中断递归上传指定本地目录到FTP资源服务器");
                    return; // 中断标记已设，提前结束循环
                }
                File localFile = new File(localParentDir, item);
                if (localFile.isFile()) {
                    uploadFileToSftp(localFile, sftpParentPath + remoteDirPath, item);
                } else {
                    String remoteSubDir = remoteDirPath + "/" + item;
                    mkdirsSftp(remoteSubDir);
                    uploadDirectory(remoteSubDir, localFile.getAbsolutePath());
                }
            }
        }
    }

    /**
     * 上传单个文件到远程服务器。
     *
     * @param localFile     本地文件对象
     * @param fileName 远程文件名称
     * @throws IOException 如果文件传输失败则抛出异常
     */
    protected void uploadFileToSftp(File localFile, String remoteDirPath, String fileName) {
        if (Thread.currentThread().isInterrupted()) {
            log.warn("定时任务被提前结束, 中断上传单个文件到远程SFTP服务器");
            return;
        }

        boolean skipUpload = false;
        // 检查远程路径下是否已存在同名文件
        List<ChannelSftp.LsEntry> entryList = sftpClient.lsEntries(remoteDirPath,
                entry -> !entry.getFilename().startsWith("."));
        for (ChannelSftp.LsEntry entry : entryList){
            if(entry.getFilename().equals(localFile.getName())){
                log.info("远程文件 [{}] Size为 [{}], 本地文件 [{}] Size为 [{}],", entry.getFilename(), entry.getAttrs().getSize(), localFile.getName(), localFile.length());
                if(entry.getAttrs().getSize() == localFile.length()){
                    // 文件已存在，并且内容大一小一致，标记为跳过上传
                    skipUpload = true;
                    log.info("文件 [{}] 已存在于远程路径 [{}], 跳过上传", localFile.getName(), fileName);
                    break;
                }
            }
        }
        if (!skipUpload) {
            // 上传文件
            sftpClient.cd(remoteDirPath); // 切换到目标目录
            sftpClient.put(localFile.getAbsolutePath(), localFile.getName()); // 上传文件
        }
    }

    /**
     * FTP服务器创建文件夹
     *
     * @param remoteDir 创建文件夹名称
     * @throws IOException 如果文件夹创建失败则抛出异常
     */
    protected void mkdirsSftp(String remoteDir){
        sftpClient.cd(this.sftpParentPath); // 切换到根目录

        String[] dirs = remoteDir.split("/");
        String currentDir = ""; // 用于累积当前路径
        for (String dir : dirs) {
            if (dir.isEmpty()) continue;
            currentDir += "/" + dir;

            try {
                sftpClient.cd(currentDir); // 尝试切换到子目录，如果目录不存在，这会抛出异常
            } catch (FtpException e) {
                // 目录不存在，需要创建
                sftpClient.mkdir(dir);
                sftpClient.cd(dir);
                log.info("创建成功: [{}], 当前目录[{}]", currentDir, sftpClient.pwd());
            }
        }
    }

    /**
     * 查找FTP服务器指定文件夹下是否存在指定文件并返回路径。
     *
     * @param drictoryPath FTP目标服务器根路径下具体某文件夹路径。
     * @param specifiedLayer 有值代表指定目录，无值检索全部。
     * @param sftpPredicate    断言，匹配满足需求的文件及目录。
     * @return FTP服务器现所有指定规则文件存放路径。
     */
    protected List<FileVo> searcherFtpCustomizedDirectory(String drictoryPath, String specifiedLayer, SftpPredicate sftpPredicate){
        return searchCustomizedDirectory(sftpParentPath + drictoryPath, specifiedLayer, sftpPredicate)
                .stream().map(s -> new FileVo(s))
                .collect(Collectors.toList());
    }

    /**
     * 主要方法，用于通过FTP客户端在指定的SFTP根目录及其子目录下查找文件路径。
     * 支持指定起始层以限制搜索范围，并会根据文件类型优先级进行筛选。
     *
     * @param sftpRootDir  FTP服务器上的根目录路径。
     * @param sftpPredicate    断言，匹配满足需求的文件及目录。
     * @return 符合条件文件路径列表，按优先级排序。
     */
    private List<String> searchCustomizedDirectory(String sftpRootDir, String specifiedLayer, SftpPredicate sftpPredicate) {
        if (sftpClient != null) {
            List<String> tilesetPaths = new ArrayList<>();
            String startDirectory = sftpRootDir + (specifiedLayer != null && !specifiedLayer.isEmpty() ? specifiedLayer + "/" : "");
            log.info("开始搜索目录查找指定文件 [{}]", startDirectory);
            List<String> directoriesToSearch = new ArrayList<>();
            directoriesToSearch.add(startDirectory);
            searchSftpDirectories(directoriesToSearch, tilesetPaths, sftpPredicate);
            return SftpConstantsUtil.pathFilter(tilesetPaths, sftpPredicate);
        }
        return null;
    }

    /**
     * 递归遍历FTP目录，查找指定目录的指定文件。
     *
     * @param startDirectory  待搜索的目录路径列表，作为递归起始。
     * @param tilesetPaths    用于收集找到的瓦片集文件路径。
     * @param sftpPredicate    断言，匹配满足需求的文件及目录。
     */
    private void searchSftpDirectories(List<String> startDirectory, List<String> tilesetPaths, SftpPredicate sftpPredicate) {
        List<String> nextDirs = new ArrayList<>();
        for (String remoteDirPath : startDirectory) {
            List<ChannelSftp.LsEntry> entryList = sftpClient.lsEntries(remoteDirPath,
                    entry -> !entry.getFilename().startsWith("."));
            for (ChannelSftp.LsEntry entry: entryList){
                SftpATTRS file = entry.getAttrs();
                if (Thread.currentThread().isInterrupted()) {
                    log.warn("定时任务被提前结束, 中断递归遍历SFTP目录查找3D瓦片集文件");
                    return;
                }
                if(file.isDir()){
                    String remoteFilePath = FileUtil.getRemoteFilePath(remoteDirPath, entry.getFilename());
                    List<ChannelSftp.LsEntry> list = sftpClient.lsEntries(remoteFilePath,
                            entry2 -> !entry2.getFilename().startsWith("."));
                    Optional<String> foundPath = SftpConstantsUtil.findFileInSftpDirectory(list, remoteFilePath, sftpPredicate);
                    foundPath.ifPresent(tilesetPaths::add);
                    if (!foundPath.isPresent()) {
                        nextDirs.add(remoteFilePath);
                    }
                }
            }
        }
        if (!nextDirs.isEmpty()) {
            searchSftpDirectories(nextDirs, tilesetPaths, sftpPredicate);
        }
    }

}
